/* ParserImpl.java

    Purpose:
        
    Description:
        
    History:
        Mon Mar  7 13:04:27 TST 2011, Created by tomyeh

Copyright (C) 2011 Potix Corporation. All Rights Reserved.

*/
package com.lt.flashcard.webservice.zest;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.zkoss.idom.Element;
import org.zkoss.idom.Item;
import org.zkoss.idom.input.SAXBuilder;
import org.zkoss.idom.util.IDOMs;
import org.zkoss.lang.Classes;
import org.zkoss.util.logging.Log;
import org.zkoss.util.resource.Locator;
import org.zkoss.xel.VariableResolver;
import org.zkoss.xel.taglib.Taglib;
import org.zkoss.xel.util.MethodFunction;
import org.zkoss.zest.ZestException;
import org.zkoss.zest.sys.ActionDefinition;
import org.zkoss.zest.sys.Configuration;
import org.zkoss.zest.sys.ErrorHandler;
import org.zkoss.zest.sys.Parser;
import org.zkoss.zest.sys.impl.ActionDefinitionImpl;
import org.zkoss.zest.sys.impl.ConfigurationImpl;
import org.zkoss.zest.sys.impl.ViewInfoProxy;

/**
 * The default implementation of the parser of the configuration file
 * (/WEB-INF/zest.xml).
 * @author tomyeh
 */
public class MyParserImpl implements Parser {
    private static final Log log = Log.lookup(MyParserImpl.class);

    public MyParserImpl() {
    }

    @Override
    public Configuration parse(URL url, Locator loc) throws Exception {
        try {
            return parse(new SAXBuilder(true, false, true).build(url).getRootElement(), loc);
        } catch (Throwable ex) {
            log.realCauseBriefly("Failed to parse " + url, ex);
            if (ex instanceof Error) {
                throw (Error) ex;
            }
            throw (Exception) ex;
        }
    }

    /** Parses the specified root element.
     */
    public Configuration parse(Element root, Locator loc) throws Exception {
        final List<ActionDefinition> defs = new LinkedList<ActionDefinition>();
        final List<Taglib> taglibs = new LinkedList<Taglib>();
        final List<Object[]> xelmtds = new LinkedList<Object[]>();
        final List<VariableResolver> resolvers = new LinkedList<VariableResolver>();
        String[] exts = null;
        ErrorHandler errh = null;
        for (Iterator it = root.getElements().iterator(); it.hasNext();) {
            final Element el = (Element) it.next();
            final String elnm = el.getName();
            if ("action".equals(elnm)) {
                defs.add(parseAction(el));
            } else if ("error-handler-class".equals(elnm)) {
                final String clsnm = el.getText(true);
                noELnorEmpty(elnm, clsnm, el);
                errh = (ErrorHandler) Classes.newInstanceByThread(clsnm);
            } else if ("extensions".equals(elnm)) {
                final String s = el.getText(true);
                noEL(elnm, s, el);
                exts = parseExtensions(s);
            } else if ("taglib".equals(elnm)) {
                final String uri = IDOMs.getRequiredAttributeValue(el, "uri");
                noELnorEmpty("uri", uri, el);
                final String prefix = IDOMs.getRequiredAttributeValue(el, "prefix");
                noELnorEmpty("prefix", prefix, el);
                taglibs.add(new Taglib(prefix, uri));
            } else if ("variable-resolver".equals(elnm)) {
                final String clsnm = IDOMs.getRequiredAttributeValue(el, "class");
                noELnorEmpty("class", clsnm, el);
                resolvers.add(0, (VariableResolver) Classes.newInstanceByThread(clsnm)); // FILO
            } else if ("xel-method".equals(elnm)) {
                parseXelMethod(xelmtds, el);
            }
        }
        return new ConfigurationImpl(defs.toArray(new ActionDefinition[defs.size()]), exts != null ? exts : new String[] { "" }, errh,
                ChainedResolver.getVariableResolver(resolvers), !taglibs.isEmpty() || !xelmtds.isEmpty() ? MyTagLibs.getFunctionMapper(taglibs, null,
                        xelmtds, loc) : null);
    }

    // parse action
    private static ActionDefinition parseAction(Element el) throws Exception {
        final Map<String, ViewInfoProxy> results = new HashMap<String, ViewInfoProxy>();
        for (Iterator it = el.getElements("result").iterator(); it.hasNext();) {
            final Element e = (Element) it.next();
            results.put(e.getAttributeValue("name"), new ViewInfoProxy(e.getAttributeValue("type"), e.getText(true)));
        }
        final String path = IDOMs.getRequiredAttributeValue(el, "path");
        noELnorEmpty("path", path, el);
        return new ActionDefinitionImpl(path, IDOMs.getRequiredAttributeValue(el, "class"), el.getAttributeValue("method"), results);
    }

    /** Parse the XEL method. */
    private static void parseXelMethod(List<Object[]> xelmtds, Element el) throws Exception {
        final String prefix = IDOMs.getRequiredAttributeValue(el, "prefix");
        noELnorEmpty("prefix", prefix, el);
        final String nm = IDOMs.getRequiredAttributeValue(el, "name");
        noELnorEmpty("name", nm, el);
        final String clsnm = IDOMs.getRequiredAttributeValue(el, "class");
        noELnorEmpty("class", clsnm, el);
        final String sig = IDOMs.getRequiredAttributeValue(el, "signature");
        noELnorEmpty("signature", sig, el);

        final Method mtd;
        try {
            final Class cls = Classes.forNameByThread(clsnm);
            mtd = Classes.getMethodBySignature(cls, sig, null);
        } catch (ClassNotFoundException ex) {
            throw new ZestException("Class not found: " + clsnm + ", " + el.getLocator());
        } catch (Exception ex) {
            throw new ZestException("Method not found: " + sig + " in " + clsnm + ", " + el.getLocator());
        }
        if ((mtd.getModifiers() & Modifier.STATIC) == 0) {
            throw new ZestException("Not a static method: " + mtd + ", " + el.getLocator());
        }
        xelmtds.add(new Object[] { prefix, nm, new MethodFunction(mtd) });
    }

    // parse extensions
    private static String[] parseExtensions(String extensions) {
        if (StringUtils.isBlank(extensions)) {
            return null;
        }
        final String[] exts = extensions.split(",");
        for (int j = 0; j < exts.length; ++j) {
            exts[j] = exts[j].trim();
            if (exts[j].length() > 0 && exts[j].charAt(0) != '.') {
                exts[j] = '.' + exts[j];
            }
        }
        return exts;
    }

    /** Whether a string is null or empty. */
    private static boolean isEmpty(String s) {
        return StringUtils.isBlank(s);
    }

    private static void noELnorEmpty(String nm, String val, Item item) throws ZestException {
        if (isEmpty(val)) {
            throw new ZestException(nm + " cannot be empty, " + item.getLocator());
        }
        noEL(nm, val, item);
    }

    private static void noEL(String nm, String val, Item item) throws ZestException {
        if (val != null && val.indexOf("${") >= 0) {
            throw new ZestException(nm + " does not support EL expressions, " + item.getLocator());
        }
    }
}

/*package*/class ChainedResolver implements VariableResolver {
    private VariableResolver[] _resolvers;

    /*package*/static VariableResolver getVariableResolver(List<VariableResolver> resolvers) {
        int sz;
        if (resolvers == null || (sz = resolvers.size()) == 0) {
            return null;
        }
        return new ChainedResolver(resolvers.toArray(new VariableResolver[sz]));
    }

    private ChainedResolver(VariableResolver[] resolvers) {
        _resolvers = resolvers;
    }

    @Override
    public Object resolveVariable(String name) {
        for (int j = 0; j < _resolvers.length; ++j) {
            final Object o = _resolvers[j].resolveVariable(name);
            if (o != null) {
                return o;
            }
        }
        return null;
    }
}
